#!/usr/bin/env python3.0

"""
 * Copyright 2009 Yogi KENT
 * 
 * This file is main part of Sotetsu.
 * 
 * Sotetsu is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 * 
 * Sotetsu is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.

"""

# standard modules
import sys, io, os
import threading
import signal
import datetime
import curses
import curses.panel
import curses.ascii
import readline
import collections
import logging
import configparser
import locale
LOG_FILE = "./sotetsu.log"
logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG)

# external modules
from .daap import daapclient
from .player import simpleplayer
from ._sotetsu import *
from ._sotetsu_utils import *
from . import wcwidth
from .ui import panels


RCFILE = "sotetsurc"

class Playlist:
	"""this class manages playlist.

	it is used by simpleplayer.Player object as to be
	iterator of file object.  Each calling __next__ returns
	one fileobject.
	"""
	def __init__(self, daap, items, pos=0):
		self.itemList = items
		self.pos = pos
		self.daap = daap
		self.fo = None
		self.item = None
		self._lock = threading.RLock()
	def __next__(self):
		if self.fo!=None:
			self.fo.close()
		self._lock.acquire()	# Lock
		if self.pos==len(self.itemList):
			raise StopIteration()
		self.item = self.itemList[self.pos]
		self.fo = self.daap.getStream(self.item)
		self.pos += 1
		self._lock.release()	# UnLock
		return self.fo
	def __iter__(self):
		return self

	def setNext(self, pos):
		"""set next position to be played.

		in the beginning, current position of itemlist was
		changed by other thread, but it has not been used
		now.
		"""
		# we have to implement synchronization.
		self._lock.acquire()	# Lock
		self.pos = pos
		self._lock.release()	# UnLock
	def lastItem(self):
		return self.item

	# lock primitive
	def lock(self):
		self._lock.acquire()
	def unlock(self):
		self._lock.release()


class CustomList(collections.UserList):
	"""it is customized List object.

	this class manages ilp list.  We can access current ilp
	to use current() function.  and can change current
	position to use next() or prev() methods.
	"""
	def __init__(self, list=[]):
		collections.UserList.__init__(self, list)
		if len(self.data)==0:
			self.currentpos = -1
		else:
			self.currentpos = 0
	def __adjustPosition(self):
		"""set currentpos to valid position."""
		if self.currentpos >= len(self.data):
			self.currentpos = len(self.data) - 1
		elif self.currentpos==-1 and len(self.data)>0:
			self.currentpos = 0
	def setCurrent(self, a):
		self.currentpos = self.index(a)
	def current(self):
		self.__adjustPosition()
		if self.currentpos==-1:
			return None
		return self.data[self.currentpos]
	def next(self):
		self.__adjustPosition()
		pos = self.currentpos + 1
		if pos >= len(self.data):
			return False
		else:
			self.currentpos = pos
			return True
	def prev(self):
		self.__adjustPosition()
		pos = self.currentpos - 1
		if pos < 0:
			return False
		else:
			self.currentpos = pos
			return True


class KeyCommand:
	"""This class performs a inputted keys to execute assigned command.

	Keys are defined in curses module, it range from 0 to KEY_MAX.
	Commands are predefined in this class, but you can set a key to
	relate with any command.
	"""
	def __init__(self, windowList, messagepanel, informationpanel, player, daap):
		self.winList = windowList
		#self.dpp = diarypagerpanel
		self.msgp = messagepanel
		self.infop = informationpanel
		self.player = player
		self.daap = daap # TODO: このdaapは消したい
		self.keyCommand_main = [ self.do_nothing for x in range(curses.KEY_MAX) ]
		#self.keyCommand_tagp = [ self.do_nothing for x in range(curses.KEY_MAX) ]
		self.commands = {
			"next-entry" : self.next_entry,
			"prev-entry" : self.prev_entry,
			"half-down"  : self.half_down,
			"half-up"    : self.half_up,
			"scroll-down": self.scroll_down,
			"scroll-up"  : self.scroll_up,
			"play"       : self.start_player,
			"stop"       : self.stop_player,
			"pause"      : self.pause_player,
			"unpause"    : self.unpause_player,
			"next-song"  : self.next_song,
			"search"     : self.search_items,
			"refine-list": self.refine_list,
			"next-list"  : self.next_list,
			"prev-list"  : self.prev_list,
			"close-list" : self.close_list,
			"refresh-window" : self.refresh_window,

			"do-nothing" : self.do_nothing,    
		}
		self.keyCommand = self.keyCommand_main

	def setCommand(self, key, commandname):
		self.keyCommand[key] = self.commands[commandname]

	def doCommand(self, key):
		return self.keyCommand[key]()
	def doCommand_name(self, name):
		return self.commands[name]()

	def canPerform(self, key):
		return 0 < key < curses.KEY_MAX

	# Commands
	def do_nothing(self):
		return False
	def next_entry(self):
		return self.winList.current().nextItem()
	def prev_entry(self):
		return self.winList.current().prevItem()
	def half_down(self):
		return self.winList.current().scrollDownHalf()
	def half_up(self):
		return self.winList.current().scrollUpHalf()
	def scroll_up(self):
		return self.winList.current().scroll(-1)
	def scroll_down(self):
		return self.winList.current().scroll(1)
	def start_player(self):
		if self.player.isPlaying():
			self.player.stop()
		# set Playlist.
		pos = self.winList.current().getCurrentPosition()
		plist =Playlist(self.daap, self.winList.current().getItems(), pos)
		self.player.setPlaylist(plist)

		# set Callback function.
		self.player.setCallbackonNext(  lambda x: self.infop.setItem(plist.lastItem()) , None)
		self.player.setCallbackonFinish(lambda x: self.infop.hide() , None)

		# this causes high CPU usage.  any idea?
		self.player.setClockCallback( lambda x,y: self.infop.updateTime(x) , None)

		# start
		self.player.play()
		#infoP.setItem(self.winList.current().getCurrentItem())
		self.infop.show()
		return True
	def stop_player(self):
		if self.player.isPlaying():
			self.player.stop()
			return True
		else:
			return False
	def pause_player(self):
		return False
	def unpause_player(self):
		return False
	def next_song(self):
		if self.player:
			self.player.next()
			return True
		else:
			return False
	def search_items(self):
		#self.winList.current().hide()
		#stdscr.move(curses.LINES-1, 0)
		#stdscr.refresh()

		str = self.msgp.getString("search> ")

		return self.winList.current().searchItem(str)

	def next_list(self):
		old = self.winList.current()
		if not self.winList.next():
			return False
		old.hide()
		self.winList.current().show()
		self.winList.current().refreshEntireWindow()
		self.infop.show() # to push info panel to the top of stack.
		return True
	def prev_list(self):
		old = self.winList.current()
		if not self.winList.prev():
			return False
		old.hide()
		self.winList.current().show()
		self.winList.current().refreshEntireWindow()
		self.infop.show()
		return True
	def close_list(self):
		if len(self.winList)==1:
			return False
		old = self.winList.current()
		self.winList.remove(old)
		old.hide()
		self.winList.current().show()
		self.infop.show()
		return False
	def do_nothing(self):
		return False
	def resize_window(self):
		# TODO: resize
		(y,x) = ilp.getmaxyx()
		ilp.resize(y-1, x-1)
		return False
	def refresh_window(self):
		self.winList.current().refreshEntireWindow()
		return True

	# for tagInputPanel
	def refine_list(self):
		tPanel = panels.tagInputPanel(50)
		#data = tagInputPanelLoop(tPanel)
		data = tPanel.mainLoop()
		# data is dict as { "artist": ..., "title": ..., "album": ...}
		if not data:
			return False

		# make a filter.
		cond = {}
		for (key, value) in data.items():
			if isinstance(value,str):
				cond[key] = re.compile(value)
		filter = lambda x: matchRegex(x, cond)

		# create child of itemListPanel.
		ilp = self.winList.current().createFiltered(filter)
		self.winList.current().hide()

		self.winList.append(ilp)
		self.winList.setCurrent(ilp)
		self.winList.current().show()
		self.winList.current().refreshEntireWindow()

		self.infop.show()
		return True

	def test_command(self):
		item = self.winList.current().getCurrentItem()
		#print(item[daapclient.SONGTIME])
		#msgp.putMessage(str(item[daapclient.SONGTIME]))
		time = datetime.timedelta(milliseconds=item[daapclient.SONGTIME])
		(min, sec) = divmod(time.seconds, 60)
		msgp.putMessage("{0:d}:{1:d}".format(min,sec))




class AttrManager:
	cname = {
		"default"	: -1,
		"black"		: curses.COLOR_BLACK,
		"red"		: curses.COLOR_RED,
		"green"		: curses.COLOR_GREEN,
		"blue"		: curses.COLOR_BLUE,
		"yellow"	: curses.COLOR_YELLOW,
		"cyan"		: curses.COLOR_CYAN,
		"magenta"	: curses.COLOR_MAGENTA,
		"white"		: curses.COLOR_WHITE,
		}
	aname = {
		"normal" : curses.A_NORMAL,
		"default": curses.A_NORMAL,
		"bright" : curses.A_BOLD,
		"reverse": curses.A_REVERSE,
		}
	def __init__(self, attrnames):
		""""initialize AttrManager.

		set attrnames, its number, curses's attributes and colors.
		"""
		if len(attrnames) > curses.COLORS:
			pass
		# color name list.
		self.attrnames = attrnames
		# color to number table.
		self.attrNum = { self.attrnames[i]: i for i in range(len(self.attrnames)) }
		# color to attribute table.
		# this can have A_NORMAL, A_REVERSE, A_BOLD or etc, but no color.　 see getAttr()
		self.attr = { self.attrnames[i]: curses.A_NORMAL for i in range(len(self.attrnames)) }

	def setAttr(self, attrname, fg, bg, attr=curses.A_NORMAL):
		"""set an attribute.

		This function sets an curses's attribute and color
		corresponding to attrname.
		attr can be..
			string: "normal"
			list of string: ("normal","bright", "underline")
			int: A_BOLD
			int: A_UNDERLINE|A_NORMAL
		"""
		if isinstance(attr, str):
			attr = self.aname[attr]
		elif isinstance(attr, list):
			a = 0
			for attrstr in attr:
				a |= self.aname[attrstr]
			attr = a

		#try:
		logging.debug("attrname={0:s}, fg={1:s}, bg={2:s}".format(attrname,fg,bg))
		curses.init_pair(self.attrNum[attrname], self.cname[fg], self.cname[bg])
		self.attr[attrname] = attr
		#except:
			# TODO:
			#logging.debug("some exception in Color.setAttr")

	def getAttr(self, attrname):
		return self.attr[attrname] | curses.color_pair(self.attrNum[attrname])

def main0(stdscr=None):
	# locale setting.
	locale.setlocale(locale.LC_ALL, "")

	daap = daapclient.Daap(sys.argv[1])
	l = (
		# 順番によって取得できない時がある? 未調査
		daapclient.ITEMID,
		daapclient.ITEMNAME,
		daapclient.SONGARTIST,
		daapclient.SONGALBUM,

		daapclient.SONGTIME, 
		#daapclient.SONGSTARTTIME, 
		#daapclient.SONGSTOPTIME, 
		)
	itemList = daap.getItemList_0(l)

	# initialize curses.
	curses.start_color()
	curses.use_default_colors()
	curses.cbreak()
	curses.noecho()
	curses.curs_set(0)
	stdscr.keypad(True)

	height = curses.LINES;
	width = curses.COLS;

	# reading resource files.
	config = configparser.RawConfigParser()
	config.read( (
		sys.prefix+"/share/sotetsu/"+RCFILE,
		os.path.expanduser('~/.'+RCFILE),
		"."+RCFILE,
		RCFILE,
		) )

	#colorInitialize(config.items("COLOR"))
	colorattr = AttrManager(( "normal", "cursor", "message", "error",))
	for (key, value) in config.items("COLOR"):
		(fg, bg, *opt) = value.split(None)
		colorattr.setAttr(key, fg, bg, opt)

	# create information panel
	iPanel = panels.infoPanelwithTime(30, (daapclient.ITEMNAME,daapclient.SONGALBUM,daapclient.SONGARTIST))

	# construct a itemListPanel.
	ilp = panels.itemListPanel(height-1, width, colorattr)
	ilp.setItems(itemList) #  should it be passed at constructer?
	ilp.refreshEntireWindow()

	ilpList = CustomList( [ilp] )

	# set status line
	msgp = panels.messagePanel(height-1, 0, 1, width, colorattr)
	msgp.putMessage("sotetsu starts.")

	# constructing player
	global player
	player = simpleplayer.Player()

	# key assignment
	keycommand = KeyCommand(ilpList, msgp, iPanel, player, daap)
	for (keyname, command) in config.items("COMMAND"):
		logging.debug("command assign  {0:s}:{1:s}".format(keyname,command))
		key = keyname2int(keyname)
		if not key:
			self.msgp.putErrorMessage("invalid key `{0:s}'".format(keyname))
			continue
		try:
			keycommand.setCommand(key, command)
		except:
			msgp.putErrorMessage("unknown command `{0:s}'".format(command))

	# they don't work, do they?
	#signal.signal( signal.SIGINT , lambda x,y: player.stop() )
	#signal.signal( signal.SIGHUP , lambda x,y: player.stop() )
	#signal.signal( signal.SIGTERM , lambda x,y: player.stop() )

	# start main loop
	logging.debug("Start main loop.")
	while True:
		ch = stdscr.getch()
		logging.debug("get key `{0}'".format(ch))
		if ch==ord('q') or ch==curses.KEY_F1:
			keycommand.doCommand_name("stop")
			break

		elif keycommand.canPerform(ch):
			rtn = keycommand.doCommand(ch)
		else:
			rtn = False

		if not rtn:
			curses.beep()
		curses.panel.update_panels()
		curses.doupdate()



def main():
	try:
		curses.wrapper(main0)
		logging.debug("end main.")
	except:
		global player
		player.stop()
		raise

